Amplifier output dB linearity check¶
This script is used to check the amplifier linearity between on screen db level difference and actual output using a GRAS reference microphone. The check is done by playing back a series of chirp sweeps and measuring the level with the GRAS at 1.5 m.
Experimental notes
Data for the experiments have been collected on June 3th, 2025.
- 192 KHz recordings from the GRAS reference mic (GRAS 40BF + 26AC preamplifier)
- 1.5 meters distance (~ far field = 10𝛌; 𝛌max = fmin; 343/2 = 171 Hz --> 10𝛌 = 1715 Hz; at 1000Hz: 4.5𝛌 = 4.5*0.343 = 1.5435 m)
HW settings:
- Harman kandon AWR 445 vol varied from -20 to -40 dB
- fireface analog out 1/2 stereo vol = 0db
- tweeter #1
- Ref mic: gras +30 db fireface channel 9, +20db channel A power module
WARNING This code is partly derived from the code:
example_w-deconvolution_runthrough.py
at this link: https://github.com/activesensingcollectives/calibrate-mic/blob/master/example_w-deconvolution_runthrough.py and makes use of:
utilities.py
both developed by Thejasvi Beleyur.
@author: Alberto Doimo
from IPython.display import Image
from IPython.display import display
path = '/home/alberto/Documents/ActiveSensingCollectives_lab/Ro-BATs/measurements/z723/array_calibration/226_238/'
display(
Image(filename=path + "PXL_20250509_10061094.jpg", width=300),
Image(filename=path + "PXL_20250509_10062195.jpg", width=300),
Image(filename=path + "rme802_matrix.png", width=700),
Image(filename=path + "rme802_mixer.png", width=700)
)
Audio analysis¶
Out signal used during the recordings: 0.2-24Khz sweeps of variable length. Having a signal with different set of sweeps of different length, allows to have repeatibility of the same signal type (useful to avarage and reduce errors), but also having the sweep energy distributed over different time frames.
IMPORTANT NOTE
The signal was reproduced from windows 11 and using Windows Media Player Software which result in outputting only the first 4 out of 5 sweeps, correctly spaced in time. Possibly there is an unexpected resampling or a digital cut of the last portion of the file, but it shouldn't affect the results, since this report only uses the first sweep for the analysis.
import numpy as np
import soundfile as sf
import matplotlib.pyplot as plt
from utilities import *
import scipy.signal as sig
fs = 192000
durns = np.array([3, 4, 5, 8] )*1e-3
chirp = []
all_sweeps = []
for durn in durns:
t = np.linspace(0, durn, int(fs*durn))
#start_f, end_f = 1e3, 20e3
start_f, end_f = 2e2, 24e3
sweep = signal.chirp(t, start_f, t[-1], end_f)
#sweep *= signal.windows.tukey(sweep.size, 0.95)
sweep *= signal.windows.tukey(sweep.size, 0.2)
sweep *= 0.8
sweep_padded = np.pad(sweep, pad_width=[int(fs*0.1)]*2, constant_values=[0,0])
all_sweeps.append(sweep_padded)
chirp.append(sweep)
- Data recordings import
DIR = "./2025-06-03/" # Directory containing the audio files
# Load the 5 sweeps of the GRAS 40BF microphone at different levels
gras_40_or, fs = sf.read('./2025-06-03/-40db_02_24k_5sweeps_channel9_192k.wav')
gras_35_or, fs = sf.read('./2025-06-03/-35db_02_24k_5sweeps_channel9_192k.wav')
gras_30_or, fs = sf.read('./2025-06-03/-30db_02_24k_5sweeps_channel9_192k.wav')
gras_25_or, fs = sf.read('./2025-06-03/-25db_02_24k_5sweeps_channel9_192k.wav')
gras_20_or, fs = sf.read('./2025-06-03/-20db_02_24k_5sweeps_channel9_192k.wav')
# Load the 1 Pa reference tone
gras_1Pa_tone, fs = sf.read('./2025-06-03/ref_tone_gras_1Pa_ch9_30dB_chA_20dB.wav', start=int(fs*0.5),
stop=int(fs*1.5))
- Matched filtering, peak extraction, RMS calculation:
- Every file is matched with the output chirp template.
- The sweep position is extracted from each file.
- The extracted peaks are showed over the matched audio.
# Define the matched filter function
def matched_filter(recording, chirp_template):
filtered_output = np.roll(signal.correlate(recording, chirp_template, 'same', method='direct'), -len(chirp_template)//2)
filtered_output *= signal.windows.tukey(filtered_output.size, 0.1)
filtered_envelope = np.abs(signal.hilbert(filtered_output))
return filtered_envelope
# Detect peaks in the matched filter output
def detect_peaks(filtered_output, sample_rate):
peaks, properties = signal.find_peaks(filtered_output, prominence=5, distance=0.2 * sample_rate)
return peaks
chirp_to_use = 0 # Use the first chirp for matching
gras_40_matched = matched_filter(gras_40_or, chirp[chirp_to_use])
gras_35_matched = matched_filter(gras_35_or, chirp[chirp_to_use])
gras_30_matched = matched_filter(gras_30_or, chirp[chirp_to_use])
gras_25_matched = matched_filter(gras_25_or, chirp[chirp_to_use])
gras_20_matched = matched_filter(gras_20_or, chirp[chirp_to_use])
# Detect peaks
peaks_gras_40 = detect_peaks(matched_filter(gras_40_matched, chirp[chirp_to_use]), fs)
peaks_gras_35 = detect_peaks(matched_filter(gras_35_matched, chirp[chirp_to_use]), fs)
peaks_gras_30 = detect_peaks(matched_filter(gras_30_matched, chirp[chirp_to_use]), fs)
peaks_gras_25 = detect_peaks(matched_filter(gras_25_matched, chirp[chirp_to_use]), fs)
peaks_gras_20 = detect_peaks(matched_filter(gras_20_matched, chirp[chirp_to_use]), fs)
print(f"Detected peaks: {len(peaks_gras_40)}")
print(f"Detected peaks: {len(peaks_gras_35)}")
print(f"Detected peaks: {len(peaks_gras_30)}")
print(f"Detected peaks: {len(peaks_gras_25)}")
print(f"Detected peaks: {len(peaks_gras_20)}")
# Plot the matched filter outputs and detected peaks for all audio files
plt.figure(figsize=(15, 10))
audio_labels = ['-40 dB', '-35 dB', '-30 dB', '-25 dB', '-20 dB']
matched_outputs = [gras_40_matched, gras_35_matched, gras_30_matched, gras_25_matched, gras_20_matched]
peaks_list = [peaks_gras_40, peaks_gras_35, peaks_gras_30, peaks_gras_25, peaks_gras_20]
for i, (matched, peaks, label) in enumerate(zip(matched_outputs, peaks_list, audio_labels), 1):
plt.subplot(5, 1, i)
t = np.linspace(0, len(matched) / fs, len(matched))
plt.plot(t, matched, label=f'Matched Output {label}')
plt.plot(peaks / fs, matched[peaks], 'ro', label='Detected Peaks')
plt.title(f'Matched Filter Output - GRAS {label}')
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.legend()
plt.tight_layout()
plt.show()
Detected peaks: 8 Detected peaks: 8 Detected peaks: 8 Detected peaks: 8 Detected peaks: 8
- Extracted audio chunks from the matched filter outputs are displayed
gras_40 = gras_40_or[int(peaks_gras_40[chirp_to_use]):int(peaks_gras_40[chirp_to_use]) + int(fs*durns[chirp_to_use])]
gras_35 = gras_35_or[int(peaks_gras_35[chirp_to_use]):int(peaks_gras_35[chirp_to_use]) + int(fs*durns[chirp_to_use])]
gras_30 = gras_30_or[int(peaks_gras_30[chirp_to_use]):int(peaks_gras_30[chirp_to_use]) + int(fs*durns[chirp_to_use])]
gras_25 = gras_25_or[int(peaks_gras_25[chirp_to_use]):int(peaks_gras_25[chirp_to_use]) + int(fs*durns[chirp_to_use])]
gras_20 = gras_20_or[int(peaks_gras_20[chirp_to_use]):int(peaks_gras_20[chirp_to_use]) + int(fs*durns[chirp_to_use])]
# Plot the extracted chirp segments for each amplifier level
plt.figure(figsize=(10, 15))
audio_segments = [gras_40, gras_35, gras_30, gras_25, gras_20]
labels = ['-40 dB', '-35 dB', '-30 dB', '-25 dB', '-20 dB']
# Find global min and max for y-axis
ymin = min([segment.min() for segment in audio_segments])
ymax = max([segment.max() for segment in audio_segments])
for i, (segment, label) in enumerate(zip(audio_segments, labels), 1):
t = np.linspace(0, len(segment) / fs, len(segment))
plt.subplot(5, 1, i)
plt.plot(t, segment)
plt.title(f'Extracted Chirp Segment: {label}')
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.ylim(ymin, ymax)
plt.tight_layout()
plt.grid()
plt.show()
- RMS is calculated for all the chunks. dB rms SPL value for each recording is calculated, using the RMS of audio chunks and rms of ref 1Pa tone, thanks to the overall flat sensitivity of the GRAS.
rms_1Pa_tone = rms(gras_1Pa_tone)
print(f'The calibration mic has a sensitivity of {np.round(rms_1Pa_tone,3)}rms/Pa. RMS relevant only for this ADC!')
# Convert from RMS to Pascals (rms equivalent)
gras_overallaudio_Parms_40 = rms(gras_40)/rms_1Pa_tone
gras_overallaudio_Parms_35 = rms(gras_35)/rms_1Pa_tone
gras_overallaudio_Parms_30 = rms(gras_30)/rms_1Pa_tone
gras_overallaudio_Parms_25 = rms(gras_25)/rms_1Pa_tone
gras_overallaudio_Parms_20 = rms(gras_20)/rms_1Pa_tone
# Convert from Pascals to db SPL
print(f'GRAS dBrms SPL measures:{pascal_to_dbspl(gras_overallaudio_Parms_40)} for -40 dB amplifier level')
print(f'GRAS dBrms SPL measures:{pascal_to_dbspl(gras_overallaudio_Parms_35)} for -35 dB amplifier level')
print(f'GRAS dBrms SPL measures:{pascal_to_dbspl(gras_overallaudio_Parms_30)} for -30 dB amplifier level')
print(f'GRAS dBrms SPL measures:{pascal_to_dbspl(gras_overallaudio_Parms_25)} for -25 dB amplifier level')
print(f'GRAS dBrms SPL measures:{pascal_to_dbspl(gras_overallaudio_Parms_20)} for -20 dB amplifier level')
# plot
db_levels = [-40, -35, -30, -25, -20]
dbspl_values = [
pascal_to_dbspl(gras_overallaudio_Parms_40),
pascal_to_dbspl(gras_overallaudio_Parms_35),
pascal_to_dbspl(gras_overallaudio_Parms_30),
pascal_to_dbspl(gras_overallaudio_Parms_25),
pascal_to_dbspl(gras_overallaudio_Parms_20)
]
min_val = min(dbspl_values)
normalized = [v - min_val for v in dbspl_values]
plt.figure(figsize=(10, 6))
plt.plot(db_levels, dbspl_values, marker='o')
for x, y, norm in zip(db_levels, dbspl_values, normalized):
plt.annotate(f'{norm[0]:.2f} dB', (x-1, y+1), textcoords="offset points", xytext=(10,5), ha='left', fontsize=9)
plt.title('Amplifier linearity check')
plt.xlabel('Amplifier output Level [dB]')
plt.ylabel('dBrms SPL')
plt.xticks(db_levels)
plt.yticks(np.array(dbspl_values).reshape(1, 5)[0])
plt.grid(True)
plt.tight_layout()
plt.show()
The calibration mic has a sensitivity of 0.029rms/Pa. RMS relevant only for this ADC! GRAS dBrms SPL measures:[84.48002855] for -40 dB amplifier level GRAS dBrms SPL measures:[89.3826392] for -35 dB amplifier level GRAS dBrms SPL measures:[94.31868564] for -30 dB amplifier level GRAS dBrms SPL measures:[99.32138425] for -25 dB amplifier level GRAS dBrms SPL measures:[104.1481838] for -20 dB amplifier level
Final Notes¶
The variability of the output difference with respect to the actual values declared on the Amplifier display is small. in this experiemnt with 20 db is off of only 0.33 dB:
104.15 - 84.48 = 19.67;
20 - 19.67 = 0.33 dB